<template>
  <RouterView v-slot="{ Component }" :class="pageClass">
    <transition v-bind="pageTransition" appear>
      <KeepAlive v-if="!resetting" :include="cache.keys" :max="max">
        <Component
          :is="resolveComponent(Component)"
          v-if="!refreshing"
          ref="current"
          :key="currentKey"
        />
      </KeepAlive>
    </transition>
  </RouterView>
</template>

<script lang="ts">
import {
  ComponentInternalInstance,
  ComponentPublicInstance,
  computed,
  defineComponent,
  nextTick,
  PropType,
  reactive,
  Ref,
  ref,
  toRefs,
  TransitionProps,
  watch
} from 'vue'
import { RouteLocationNormalizedLoaded, useRoute } from 'vue-router'
import { resolveTransition } from './utils'

/**
 * 缓存控制
 * @param {Ref<string>} currentKey 当前 key
 */
function useCache(currentKey: Ref<string>) {
  /** 缓存组件的 key 集合 */
  const keys: Ref<string[]> = ref([])

  return reactive({
    keys,

    /**
     * 移除缓存
     * @param {string} key 缓存 key
     */
    remove(key: string = currentKey.value) {
      if (!key) return

      const idx = keys.value.indexOf(key)

      idx > -1 && keys.value.splice(idx, 1)
    },

    /**
     * 添加缓存
     * @param {string} key 缓存 key
     */
    add(key: string = currentKey.value) {
      if (!keys.value.includes(key)) {
        keys.value.push(key)
      }
    }
  })
}

/**
 * 操作
 * @param {object} cache 缓存控制
 * @param {map} componentMap 组件集合
 */
function useOperate(
  cache: ReturnType<typeof useCache>,
  componentMap: Map<string, ComponentInternalInstance>
) {
  /** 刷新中 */
  const refreshing = ref(false)

  /** 触发刷新 */
  async function triggerRefresh() {
    refreshing.value = true
    await nextTick()
    refreshing.value = false
  }

  /** 重置中 */
  const resetting = ref(false)

  /** 触发重置 */
  async function triggerReset() {
    resetting.value = true
    cache.keys = []
    componentMap.clear()

    await nextTick()
    resetting.value = false
  }

  return reactive({
    refreshing,

    triggerRefresh,

    /** 刷新 */
    async refresh() {
      cache.remove()
      await triggerRefresh()
      cache.add()
    },

    resetting,

    triggerReset,

    /** 重置缓存 */
    async reset() {
      await triggerReset()
      cache.add()
    }
  })
}

/** 路由缓存控制 */
export default defineComponent({
  name: 'RouterAlive',

  provide() {
    /** 提供实例给子组件调用 */
    return {
      RouterAlive: this
    }
  },

  props: {
    /** 默认是否开启缓存 */
    keepAlive: {
      type: Boolean,
      default: false
    },

    /** 是否复用路由组件 */
    reuse: {
      type: Boolean,
      default: false
    },

    /** 最大缓存数，0 则不限制 */
    max: {
      type: Number,
      default: 0
    },

    /** 页面 class */
    pageClass: {
      type: [Array, Object, String],
      default: 'router-alive-page'
    },

    /** 页面滚动元素选择器 */
    pageScroller: {
      type: String,
      default: ''
    },

    /** 过渡效果 */
    transition: {
      type: [String, Object] as PropType<string | TransitionProps>,
      default: () => ({
        name: 'router-page-swap',
        mode: 'out-in'
      })
    }
  },

  setup(props) {
    /** 当前页面组件 */
    const current: Ref<null | ComponentPublicInstance> = ref(null)

    /** 当前路由 */
    const route = useRoute()

    /** 当前页面组件缓存 key */
    const currentKey = computed(() => route.fullPath)

    /** 缓存控制 */
    const cache = useCache(currentKey)

    watch(
      currentKey,
      val => {
        /** 记录需要缓存的组件 */
        val && cache.add(val)
      },
      { immediate: true }
    )

    /** 组件集合 */
    const componentMap: Map<string, ComponentInternalInstance> = new Map()

    return {
      current,
      currentKey,

      /**
       * 处理路由组件
       * @description
       * 更改组件 name 为缓存 key，以便用 `KeepAlive` 的 `include` 控制缓存
       */
      resolveComponent(component: ComponentInternalInstance) {
        if (component) {
          const key = currentKey.value
          let target = componentMap.get(key)

          if (!target) {
            /** 拷贝组件方便多缓存实例 */
            target = {
              ...component,
              type: { ...component.type, name: key }
            }
            componentMap.set(key, target)
          }

          return target
        }

        return component
      },

      cache,

      /** 操作 */
      ...toRefs(useOperate(cache, componentMap)),

      /** 页面过渡效果 */
      pageTransition: computed(() => resolveTransition(props.transition))
    }
  }
})
</script>

<style lang="scss">
@import '../src/assets/css/transition.scss';
</style>
